﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;

namespace Ooaavee.Scripting
{
    /// <summary>
    /// The data of one referenced assembly.
    /// </summary>
    [Serializable]
    public sealed class Reference : ICloneable
    {
        private Collection<string> _namespaces;

        /// <summary>
        /// All needed namespaces from this referenced assembly.
        /// </summary>
        public Collection<string> Namespaces
        {
            get
            {
                if (_namespaces == null)
                {
                    _namespaces = new Collection<string>();
                }
                return _namespaces;
            }
        }

        /// <summary>
        /// Assembly file.
        /// </summary>
        public FileInfo AssemblyFile { set; get; }

        /// <summary>
        /// Is this assembly located outside the GAC.
        /// </summary>
        public bool IsOutsideGac { set; get; }

        /// <summary>
        /// The assembly full name.
        /// </summary>
        public string AssemblyFullName { set; get; }

        /// <summary>
        /// Returns a deep copy of this object.
        /// </summary>
        /// <returns>a deep copy</returns>
        object ICloneable.Clone()
        {
            Reference clone = new Reference();

            clone.AssemblyFile = AssemblyFile;
            clone.IsOutsideGac = IsOutsideGac;
            clone.AssemblyFullName = AssemblyFullName;

            foreach (string ns in Namespaces)
            {
                clone.Namespaces.Add(ns);
            }

            return clone;
        }

        /// <summary>
        /// Gets the default .NET framework assemblies that are always referenced.
        /// </summary>
        /// <returns>List of assemblies.</returns>
        internal static List<Assembly> GetDefaultFrameworkAssemblies()
        {
            List<Assembly> assemblies = new List<Assembly>();

            var assemblyNames = new[]
                {
                    "System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                    "System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                    "System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                    "System.Data.DataSetExtensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                    "System.Deployment, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
                    "System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
                    "System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                    "System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
                    "System.Xml.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
                };

            foreach (string assemblyName in assemblyNames)
            {
                Assembly assembly = Assembly.Load(assemblyName);
                assemblies.Add(assembly);
            }

            return assemblies;
        }

        /// <summary>
        /// Creates a new <see cref="Reference"/> object.
        /// </summary>
        /// <param name="assembly">The assembly</param>
        /// <returns>Reference</returns>
        public static Reference CreateReference(Assembly assembly)
        {
            if (assembly == null)
            {
                throw new ArgumentNullException("assembly");                
            }
            
            Reference reference = new Reference();

            // Assembly file
            reference.AssemblyFile = new FileInfo(assembly.CodeBase.Replace(@"file:///", String.Empty));

            // Assembly full name
            reference.AssemblyFullName = assembly.FullName;

            // Get namespaces.
            foreach (Type type in assembly.GetTypes())
            {
                string ns;
                if (TryGetNamespace(type, out ns))
                {
                    if (reference.Namespaces.Contains(ns) == false)
                    {
                        reference.Namespaces.Add(ns);
                    }
                }
            }

            return reference;
        }

        /// <summary>
        /// Tries to get the namespace of the Type and returns a value that indicates whether succeeded.
        /// 
        /// The namespace will not be found, if the Type isn't visible outside it's Assembly or if the 
        /// Types has no namespace or represents a generic parameter.
        /// </summary>
        /// <param name="type">Type</param>
        /// <param name="ns">Namespace</param>
        /// <returns>True if succeed.</returns>
        private static bool TryGetNamespace(Type type, out string ns)
        {
            ns = null;

            if (type != null && type.IsVisible)
            {
                try
                {
                    if (type.IsNestedPublic)
                    {
                        ns = type.FullName;
                        if (ns != null)
                        {
                            int index = ns.LastIndexOf('.');

                            if (index > 0)
                            {
                                ns = ns.Substring(0, index);
                            }
                            else
                            {
                                ns = null;
                            }
                        }
                    }
                    else
                    {
                        ns = type.Namespace;
                    }
                }
                catch (Exception)
                {
                    ns = null;
                }
            }

            return ns != null;
        }
    }
}